channel 用例大全
本文摘自 https://gfw.go101.org/article/channel-use-cases.html
不要通过共享内存来通信,而是通过通信来共享内存
本文将展示很多 channel 用例。 希望这篇文章能够说服你接收下面的观点:
- 使用 channel 进行异步和并发编程是简单和惬意的;
- channel 同步技术比被很多其它语言采用的其它同步方案(比如角色模型和 async/await 模式)有着更多的应用场景和更多的使用变种。
请注意,本文的目的是展示尽量多的 channel 用例。但是,我们应该知道 channel 并不是 Go 支持的唯一同步技术,并且 channel 并不是在任何情况下都是最佳的同步技术。
将 channel 用做 future/promise
很多其它流行语言支持 future/promise 来实现异步(并发)编程。 Future/promise 常常用在请求 / 回应场合。
将只读 channel 用做函数返回结果
在下面这个例子中,sumSquares
函数调用的两个实参请求并发进行。 每个通道读取操作将阻塞到请求返回结果为止。 两个实参总共需要大约 3 秒钟(而不是 6 秒钟)准备完毕(以较慢的一个为准)。
package main
import (
"time"
"math/rand"
"fmt"
)
func longTimeRequest() <-chan int32 {
r := make(chan int32)
go func() {
time.Sleep(time.Second * 3) // 模拟一个工作负载
r <- rand.Int31n(100)
}()
return r
}
func sumSquares(a, b int32) int32 {
return a*a + b*b
}
func main() {
rand.Seed(time.Now().UnixNano())
a, b := longTimeRequest(), longTimeRequest()
fmt.Println(sumSquares(<-a, <-b))
}
因为 go 修饰的函数的返回值是无效的,所以在设计 longTimeRequest()
时,把 go 写在了函数内,而不是 go longTimeRequest()
,使其支持返回一个单向通道的作用。
将只写 channel 用做函数实参
和上例一样,在下面这个例子中,sumSquares
函数调用的两个实参的请求也是并发进行的。 和上例不同的是longTimeRequest
函数接收一个单向发送通道类型参数而不是返回一个单向接收通道结果。
package main
import (
"time"
"math/rand"
"fmt"
)
func longTimeRequest(r chan<- int32) {
time.Sleep(time.Second * 3) // 模拟一个工作负载
r <- rand.Int31n(100)
}
func sumSquares(a, b int32) int32 {
return a*a + b*b
}
func main() {
rand.Seed(time.Now().UnixNano())
ra, rb := make(chan int32), make(chan int32)
go longTimeRequest(ra)
go longTimeRequest(rb)
fmt.Println(sumSquares(<-ra, <-rb))
}
对于上面这个特定的例子,我们可以只使用一个通道来接收回应结果,因为两个参数的作用是对等的。
...
results := make(chan int32, 2) // 缓冲与否不重要
go longTimeRequest(results)
go longTimeRequest(results)
fmt.Println(sumSquares(<-results, <-results))
}
这可以看作是后面将要提到的数据聚合的一个应用。
这种思路相对不是很好理解
采用最快回应
本用例可以看作是上例中只使用一个通道变种的增强。
有时候,一份数据可能同时从多个数据源获取。这些数据源将返回相同的数据。 因为各种因素,这些数据源的回应速度参差不一,甚至某个特定数据源的多次回应速度之间也可能相差很大。 同时从多个数据源获取一份相同的数据可以有效保障低延迟。我们只需采用最快的回应并舍弃其它较慢回应。
注意:如果有 N 个数据源,为了防止被舍弃的回应对应的协程永久阻塞,则传输数据用的通道必须为一个容量至少为 N-1 的缓冲通道。
package main
import (
"fmt"
"time"
"math/rand"
)
func source(c chan<- int32) {
ra, rb := rand.Int31(), rand.Intn(3) + 1
// 睡眠1秒/2秒/3秒
time.Sleep(time.Duration(rb) * time.Second)
c <- ra
}
func main() {
rand.Seed(time.Now().UnixNano())
startTime := time.Now()
c := make(chan int32, 5) // 必须用一个缓冲通道
for i := 0; i < cap(c); i++ {
go source(c)
}
rnd := <- c // 只有第一个回应被使用了
fmt.Println(time.Since(startTime))
fmt.Println(rnd)
}
这样做挺蠢的,因为被舍弃的回应始终在运行直到结束,会占用相当多的资源,更好的方式是在一个协程被接收后,主程序通知其他协程终止运行,这其实是一个一对多通知的模型。
“采用最快回应”用例还有一些其它实现方式,本文后面将会谈及。
更多 “请求 / 回应” 用例变种
做为函数参数和返回结果使用的通道可以是缓冲的,从而使得请求协程不需阻塞到它所发送的数据被接收为止。
有时,一个请求可能并不保证返回一份有效的数据。对于这种情形,我们可以使用一个形如 struct{v T; err error}
的结构体类型或者一个空接口类型做为通道的元素类型以用来区分回应的值是否有效。
有时,一个请求可能需要比预期更长的用时才能回应,甚至永远都得不到回应。 我们可以使用本文后面将要介绍的超时机制来应对这样的情况。
有时,回应方可能会不断地返回一系列值,这也同时属于后面将要介绍的数据流的一个用例。
使用 channel 实现通知
通知可以被看作是特殊的请求 / 回应用例。在一个通知用例中,我们并不关心回应的值,我们只关心回应是否已发生。 所以我们常常使用空结构体类型 struct{}
来做为通道的元素类型,因为空结构体类型的尺寸为零,能够节省一些内存(虽然常常很少量)。